using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;

namespace AOPCompiler
{
    class Program
    {
        static void Main(string[] args)
        {
            // TODO: support compliation of .csproj and .sln
            // TODO: hack at semantic layer or later. solve QualifiedName/Name issue; solve AOP attribute in dll issue
            // TODO: support multi AOPAttributes per method
            // TODO: integrate with VS

            var source = File.ReadAllText("MyProgram.cs");

            var tree = SyntaxFactory.ParseSyntaxTree(source);

            var injection = new MethodAOPInjection((CompilationUnitSyntax)tree.GetRoot());

            var compilation = CSharpCompilation.Create(
                "MySource.exe",
                options: new CSharpCompilationOptions(OutputKind.ConsoleApplication),
                syntaxTrees: new[] { injection.GetInjectedSyntaxTree() },
                references: new[] { new MetadataFileReference(typeof(object).Assembly.Location) });

            using (var stream = new FileStream("MySource.exe", FileMode.Create, FileAccess.ReadWrite))
            {
                var compileResult = compilation.Emit(stream);
            }
            Process p = Process.Start("MySource.exe");
            p.WaitForExit();
        }
    }

    class MethodAOPAttributeScannar : SyntaxWalker
    {
        readonly public List<string> MethodAOPAttributeNames = new List<string>();
        readonly public Dictionary<string, SyntaxList<StatementSyntax>> OnExecutingStatements = new Dictionary<string, SyntaxList<StatementSyntax>>();
        readonly public Dictionary<string, SyntaxList<StatementSyntax>> OnExecutedStatements = new Dictionary<string, SyntaxList<StatementSyntax>>();

        public override void Visit(SyntaxNode node)
        {
            if (node.CSharpKind() == SyntaxKind.ClassDeclaration)
            {
                var classDeclarationSyntax = node as ClassDeclarationSyntax;
                if (classDeclarationSyntax.IsAttribute("MethodAOPAttribute"))
                {
                    var className = classDeclarationSyntax.Identifier.ValueText;
                    if (!MethodAOPAttributeNames.Contains(className))
                    {
                        MethodAOPAttributeNames.Add(className);
                        OnExecutingStatements[className] = classDeclarationSyntax.GetMethodStatements("OnExecuting");
                        OnExecutedStatements[className] = classDeclarationSyntax.GetMethodStatements("OnExecuted");
                    }
                }
            }
            base.Visit(node);
        }
    }

    class MethodAOPInjection
    {
        private CompilationUnitSyntax originRoot;
        private MethodAOPAttributeScannar scanner;
        private Dictionary<string, string> methodAOPs;

        public MethodAOPInjection(CompilationUnitSyntax root)
        {
            this.originRoot = root;
            this.scanner = new MethodAOPAttributeScannar();
            this.scanner.Visit(root);
            this.methodAOPs = new Dictionary<string, string>();
        }

        public SyntaxTree GetInjectedSyntaxTree()
        {
            var pendingInjectMethods = this.originRoot.DescendantNodes().OfType<MethodDeclarationSyntax>()
                                                                        .Where(node =>
                                                                                {
                                                                                    foreach (var attributesList in node.AttributeLists)
                                                                                    {
                                                                                        foreach (var attribute in attributesList.Attributes)
                                                                                        {
                                                                                            // TODO Handle QualifiedName
                                                                                            var name = attribute.Name as IdentifierNameSyntax;
                                                                                            if (name != null)
                                                                                            {
                                                                                                // TODO support multi MethodAOPAttribute per method
                                                                                                var validateName = name.Identifier.ValueText.EndsWith("Attribute") ? name.Identifier.ValueText : string.Concat(name.Identifier.ValueText, "Attribute");
                                                                                                if (scanner.MethodAOPAttributeNames.Contains(validateName))
                                                                                                {
                                                                                                    methodAOPs.Add(node.Identifier.ValueText, validateName);
                                                                                                    return true;
                                                                                                }
                                                                                            }
                                                                                        }
                                                                                    }
                                                                                    return false;
                                                                                })
                                                                         .ToList();

            var injectedRoot = this.originRoot;
            foreach (var method in pendingInjectMethods)
            {
                var oldStatments = method.Body.Statements;
                var oldBody = method.Body;
                var attributeName = this.methodAOPs[method.Identifier.ValueText];
                var onExecutingStatements = scanner.OnExecutingStatements[attributeName];
                var onExecutedStatements = scanner.OnExecutedStatements[attributeName];
                var newStatments = SyntaxFactory.List<StatementSyntax>(onExecutingStatements.Concat(oldStatments).Concat(onExecutedStatements));
                var newBody = oldBody.WithStatements(newStatments);
                injectedRoot = injectedRoot.ReplaceNode(oldBody, newBody);
            }
            return injectedRoot.SyntaxTree;
        }
    }
}
